Skip to content

fix: harden NL2SQLTool — read-only default, query validation, parameterized queries#5311

Merged
joaomdmoura merged 15 commits intomainfrom
fix/nl2sql-security-hardening
Apr 9, 2026
Merged

fix: harden NL2SQLTool — read-only default, query validation, parameterized queries#5311
joaomdmoura merged 15 commits intomainfrom
fix/nl2sql-security-hardening

Conversation

@alex-clawd
Copy link
Copy Markdown
Contributor

@alex-clawd alex-clawd commented Apr 7, 2026

Hardens NL2SQLTool against SQL injection and unauthorized write operations.

Changes

  • Read-only by default: Only SELECT/SHOW/DESCRIBE/EXPLAIN allowed. Write operations (INSERT/UPDATE/DELETE/DROP/etc.) blocked unless allow_dml=True.
  • Parameterized queries: Fixed f-string SQL injection in _fetch_all_available_columns() — now uses SQLAlchemy :param binding.
  • No unnecessary commits: session.commit() only called when allow_dml=True and query is a write operation.
  • Query validation: New _validate_query() method parses SQL command type before execution.
  • Escape hatch: CREWAI_NL2SQL_ALLOW_DML=true env var for backward compat.

Breaking Changes

  • Write queries (INSERT/UPDATE/DELETE/DROP) now blocked by default
  • Set allow_dml=True or CREWAI_NL2SQL_ALLOW_DML=true to re-enable

Tests

Comprehensive tests with in-memory SQLite covering read-only enforcement, DML blocking, parameterized queries, and escape hatch.


Note

Medium Risk
Changes default behavior to block write queries and adds non-trivial SQL parsing/validation (CTEs, multi-statement, EXPLAIN ANALYZE), which could break existing workflows or misclassify queries despite improving safety.

Overview
Hardens NL2SQLTool to be read-only by default by introducing an allow_dml flag (and CREWAI_NL2SQL_ALLOW_DML=true override) and enforcing statement-level validation that blocks write operations, multi-statement ; queries, writable CTEs (WITH ... DELETE/INSERT/...), and EXPLAIN ANALYZE on write statements unless explicitly enabled.

Improves SQL-injection resilience by parameterizing _fetch_all_available_columns (removing f-string interpolation), only committing transactions when a write is actually executed, updates tool metadata/specs and multilingual docs to reflect the new behavior, and adds extensive security-focused tests covering these cases.

Reviewed by Cursor Bugbot for commit eb9227a. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions bot added the size/L label Apr 7, 2026
Comment thread lib/crewai-tools/tests/tools/test_nl2sql_security.py Fixed
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
@alex-clawd alex-clawd force-pushed the fix/nl2sql-security-hardening branch 2 times, most recently from b052a72 to e28604a Compare April 7, 2026 06:29
@alex-clawd alex-clawd force-pushed the fix/nl2sql-security-hardening branch 2 times, most recently from 2ad9d93 to 34a3d24 Compare April 7, 2026 06:49
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
@theCyberTech
Copy link
Copy Markdown
Member

theCyberTech commented Apr 7, 2026

Can we also update tool docs to reflect this new state? @alex-clawd

@alex-clawd alex-clawd force-pushed the fix/nl2sql-security-hardening branch from 9df00c6 to 640b807 Compare April 7, 2026 07:16
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
@alex-clawd alex-clawd force-pushed the fix/nl2sql-security-hardening branch from e6f4975 to 82c86bc Compare April 7, 2026 07:54
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
Comment thread lib/crewai-tools/tests/tools/test_nl2sql_security.py Outdated
@alex-clawd alex-clawd force-pushed the fix/nl2sql-security-hardening branch from 39278ba to 95b91af Compare April 7, 2026 15:42
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
@alex-clawd alex-clawd force-pushed the fix/nl2sql-security-hardening branch from 33cf1fe to 993441f Compare April 7, 2026 16:17
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py Outdated
alex-clawd and others added 11 commits April 8, 2026 22:42
… query validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused `sessionmaker` import from test_nl2sql_security.py
- Use `Self` return type on `_apply_env_override` (fixes UP037/F821)
- Fix ruff errors auto-fixed in lib/crewai (UP007, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tion

- Add missing write commands: UPSERT, LOAD, COPY, VACUUM, ANALYZE,
  ANALYSE, REINDEX, CLUSTER, REFRESH, COMMENT, SET, RESET
- _validate_query() now splits on ';' and validates each statement
  independently; multi-statement queries are rejected outright in
  read-only mode to prevent 'SELECT 1; DROP TABLE users' bypass
- Extract single-statement logic into _validate_statement() helper
- Add TestSemicolonInjection and TestExtendedWriteCommands test classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…LYZE, multi-stmt commit)

- Remove WITH from _READ_ONLY_COMMANDS; scan CTE body for write keywords so
  writable CTEs like `WITH d AS (DELETE …) SELECT …` are blocked in read-only mode.
- EXPLAIN ANALYZE/ANALYSE now resolves the underlying command; EXPLAIN ANALYZE DELETE
  is treated as a write and blocked in read-only mode.
- execute_sql commit decision now checks ALL semicolon-separated statements so
  a SELECT-first batch like `SELECT 1; DROP TABLE t` still triggers a commit
  when allow_dml=True.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
_validate_statement now strips parenthesized options from EXPLAIN (e.g.
EXPLAIN (ANALYZE) DELETE, EXPLAIN (ANALYZE, VERBOSE) DELETE) before
checking whether ANALYZE/ANALYSE is present — closing the bypass where
the options-list form was silently allowed in read-only mode.

Adds three new tests:
  - EXPLAIN (ANALYZE) DELETE  → blocked
  - EXPLAIN (ANALYZE, VERBOSE) DELETE  → blocked
  - EXPLAIN (VERBOSE) SELECT  → allowed

Also removes the unused _seed_db helper from test_nl2sql_security.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Replace naive token-set matching with positional AS() body inspection
  to avoid false positives on column names like 'comment', 'set', 'reset'
- Fix execute_sql commit logic to detect writable CTEs (WITH + DELETE/INSERT)
  not just top-level write commands
- Add tests for false positive cases and writable CTE commit behavior
- Format nl2sql_tool.py to pass ruff format check
- WITH cte AS (SELECT 1) DELETE FROM users now correctly blocked
- AS followed by newline/tab/multi-space before ( now detected
- execute_sql commit logic updated for both cases
- 4 new tests
…ommit logic for EXPLAIN ANALYZE

- EXPLAIN handler now consumes all known options (ANALYZE, ANALYSE, VERBOSE) before
  extracting the real command, fixing 'EXPLAIN ANALYZE VERBOSE SELECT' being blocked
- Paren walker in _extract_main_query_after_cte now skips string literals, preventing
  'WITH cte AS (SELECT '\''('\'' FROM t) DELETE FROM users' from bypassing detection
- _is_write_stmt in execute_sql now resolves EXPLAIN ANALYZE to underlying command
  via _resolve_explain_command, ensuring session.commit() fires for write operations
- 10 new tests covering all three fixes
@alex-clawd alex-clawd force-pushed the fix/nl2sql-security-hardening branch from 88ffeb3 to e2eca93 Compare April 9, 2026 05:42
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
…nown CTE commands, bump langchain-core

- Refactor _validate_statement to use _resolve_explain_command (single source of truth)
- _iter_as_paren_matches skips string literals so 'AS (' in data doesn't confuse CTE detection
- Unknown commands after CTE definitions now blocked in read-only mode
- Bump langchain-core override to >=1.2.28 (GHSA-926x-3r5x-gfhw)
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit eb9227a. Configure here.

Comment thread lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py
@joaomdmoura joaomdmoura merged commit ce56472 into main Apr 9, 2026
57 checks passed
@joaomdmoura joaomdmoura deleted the fix/nl2sql-security-hardening branch April 9, 2026 06:21
volkanozyildirim pushed a commit to volkanozyildirim/crew-ai that referenced this pull request Apr 15, 2026
…erized queries (crewAIInc#5311)

* fix: harden NL2SQLTool — read-only by default, parameterized queries, query validation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address CI lint failures and remove unused import

- Remove unused `sessionmaker` import from test_nl2sql_security.py
- Use `Self` return type on `_apply_env_override` (fixes UP037/F821)
- Fix ruff errors auto-fixed in lib/crewai (UP007, etc.)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: expand _WRITE_COMMANDS and block multi-statement semicolon injection

- Add missing write commands: UPSERT, LOAD, COPY, VACUUM, ANALYZE,
  ANALYSE, REINDEX, CLUSTER, REFRESH, COMMENT, SET, RESET
- _validate_query() now splits on ';' and validates each statement
  independently; multi-statement queries are rejected outright in
  read-only mode to prevent 'SELECT 1; DROP TABLE users' bypass
- Extract single-statement logic into _validate_statement() helper
- Add TestSemicolonInjection and TestExtendedWriteCommands test classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* ci: retrigger

* fix: use typing_extensions.Self for Python 3.10 compat

* chore: update tool specifications

* docs: document NL2SQLTool read-only default and DML configuration

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: close three NL2SQLTool security gaps (writable CTEs, EXPLAIN ANALYZE, multi-stmt commit)

- Remove WITH from _READ_ONLY_COMMANDS; scan CTE body for write keywords so
  writable CTEs like `WITH d AS (DELETE …) SELECT …` are blocked in read-only mode.
- EXPLAIN ANALYZE/ANALYSE now resolves the underlying command; EXPLAIN ANALYZE DELETE
  is treated as a write and blocked in read-only mode.
- execute_sql commit decision now checks ALL semicolon-separated statements so
  a SELECT-first batch like `SELECT 1; DROP TABLE t` still triggers a commit
  when allow_dml=True.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: handle parenthesized EXPLAIN options syntax; remove unused _seed_db

_validate_statement now strips parenthesized options from EXPLAIN (e.g.
EXPLAIN (ANALYZE) DELETE, EXPLAIN (ANALYZE, VERBOSE) DELETE) before
checking whether ANALYZE/ANALYSE is present — closing the bypass where
the options-list form was silently allowed in read-only mode.

Adds three new tests:
  - EXPLAIN (ANALYZE) DELETE  → blocked
  - EXPLAIN (ANALYZE, VERBOSE) DELETE  → blocked
  - EXPLAIN (VERBOSE) SELECT  → allowed

Also removes the unused _seed_db helper from test_nl2sql_security.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: update tool specifications

* fix: smarter CTE write detection, fix commit logic for writable CTEs

- Replace naive token-set matching with positional AS() body inspection
  to avoid false positives on column names like 'comment', 'set', 'reset'
- Fix execute_sql commit logic to detect writable CTEs (WITH + DELETE/INSERT)
  not just top-level write commands
- Add tests for false positive cases and writable CTE commit behavior
- Format nl2sql_tool.py to pass ruff format check

* fix: catch write commands in CTE main query + handle whitespace in AS()

- WITH cte AS (SELECT 1) DELETE FROM users now correctly blocked
- AS followed by newline/tab/multi-space before ( now detected
- execute_sql commit logic updated for both cases
- 4 new tests

* fix: EXPLAIN ANALYZE VERBOSE handling, string literal paren bypass, commit logic for EXPLAIN ANALYZE

- EXPLAIN handler now consumes all known options (ANALYZE, ANALYSE, VERBOSE) before
  extracting the real command, fixing 'EXPLAIN ANALYZE VERBOSE SELECT' being blocked
- Paren walker in _extract_main_query_after_cte now skips string literals, preventing
  'WITH cte AS (SELECT '\''('\'' FROM t) DELETE FROM users' from bypassing detection
- _is_write_stmt in execute_sql now resolves EXPLAIN ANALYZE to underlying command
  via _resolve_explain_command, ensuring session.commit() fires for write operations
- 10 new tests covering all three fixes

* fix: deduplicate EXPLAIN parsing, fix AS( regex in strings, block unknown CTE commands, bump langchain-core

- Refactor _validate_statement to use _resolve_explain_command (single source of truth)
- _iter_as_paren_matches skips string literals so 'AS (' in data doesn't confuse CTE detection
- Unknown commands after CTE definitions now blocked in read-only mode
- Bump langchain-core override to >=1.2.28 (GHSA-926x-3r5x-gfhw)

* fix: add return type annotation to _iter_as_paren_matches

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants